/*
 * Copyright 2012-2013 Finbudget.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package finbudgettable.client.ui.grid.column;

import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.FieldUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.cellview.client.Column;
import com.google.gwt.user.cellview.client.ColumnSortList;
import com.google.gwt.user.cellview.client.Header;

import finbudget.client.ui.table.Record;
import finbudgettable.client.ui.grid.CellGrid;
import finbudgettable.client.ui.grid.Resizable;
import finbudgettable.client.ui.grid.cell.*;
import finbudgettable.client.ui.grid.cell.footer.EmptyFooter;
import finbudgettable.client.ui.grid.cell.status.StatusCell;
import finbudgettable.client.ui.grid.cell.status.StatusColumn;
import finbudgettable.client.ui.grid.dnd.DragHelper;
import finbudgettable.client.ui.grid.dnd.Draggable;
import finbudgettable.client.ui.grid.dnd.DropHelper;
import finbudgettable.client.ui.grid.event.*;
import finbudgettable.client.ui.grid.style.CssUtils;

import java.util.Comparator;

public abstract class GridColumn<C> extends Column<Record, C> {

    static final Resources RESOURCES;

    static {
    	RESOURCES = GWT.create(Resources.class);
        RESOURCES.style().ensureInjected();
    }

    private final CellGrid grid;
    private final String fieldName;
    private final String[] cssRules;
    private final String cssClassName;

    protected GridColumn(Builder<? extends Cell<C>, C> builder) {
        super(builder == null ? null : builder.cell);
        this.grid = builder.grid;
        this.fieldName = builder.fieldName;
        cssClassName = grid.getCssController().getNextClassName();
        this.setCellStyleNames(getCellStyleNames() + " " + cssClassName);
        cssRules = createCssRules(cssClassName);
    }

    public String getFieldName() {
        return fieldName;
    }

    @Override
    public void setFieldUpdater(final FieldUpdater<Record, C> fieldUpdater) {
        super.setFieldUpdater(new FieldUpdater<Record, C>() {
            @Override
            public void update(int index, Record record, C value) {
                if (isValueChanged(record, value)) {
                    grid.fireEvent(new RecordChangingEvent(record, fieldName, value));
                    record.isChanged = true;
                    fieldUpdater.update(index, record, value);
                    grid.fireEvent(new RecordChangedEvent(record));
                }
            }
        });
    }

    /**
     * Some columns might want to set field updater directly. For example, permissions column
     * doesn't change the value but calls a specified presenter using record's key.
     * @param fieldUpdater
     */
    protected void setFieldUpdaterDirectly(FieldUpdater<Record, C> fieldUpdater) {
    	super.setFieldUpdater(fieldUpdater);
    }

    /**
     * Checks if field's value of the record is not the same as the value
     */
    protected abstract boolean isValueChanged(Record record, C value);

    /**
     * Generates css rule names and apply them to the current document
     *
     * @param cssClassName The base class name
     * @return The list of generated rules
     */
    private String[] createCssRules(String cssClassName) {
        return new String[] {
                "th > div." + cssClassName,
                "td." + cssClassName,
                "td." + cssClassName + " > div"};
    }

    protected String getCellStyleNames() {
        return "";
    }

    public String getCssClassName() {
        return cssClassName;
    }

    public CellGrid getGrid() {
        return grid;
    }

    private String width;

    /**
     * Adjust a column's width to the defined inner value
     */
    public void adjustWidth() {
        for (String ruleName : cssRules) {
            CssUtils.setStylePropertyDeffered(ruleName, "minWidth", width);
            CssUtils.setStylePropertyDeffered(ruleName, "maxWidth", width);
        }
    }

    /**
     * Uses only by then grid.
     */
    public void setWidth(String width) {
        this.width = width;
        adjustWidth();
    }

    public String getWidth() {
        return width;
    }

    public SortOrder getSortOrder() {
        ColumnSortList sortList = grid.getColumnSortList();
        if (sortList.size() < 1 || !sortList.get(0).getColumn().equals(this)) {
            return SortOrder.UNKNOWN;
        }
        if (sortList.get(0).isAscending()) {
            return SortOrder.ASC;
        } else {
            return SortOrder.DESC;
        }
    }

    @Override
    public String toString() {
        return "GridColumn{" +
                "fieldName='" + fieldName + '\'' +
                '}';
    }

    public boolean resetFocus(Cell.Context context, Element parent, Record rec) {
        return getCell().resetFocus(context, parent, getValue(rec));
    }

    @Override
    public void render(Cell.Context context, Record object, SafeHtmlBuilder sb) {
        super.render(context, object, sb);
        adjustWidth();
    }

    public boolean canBeHidden() {
        return true;
    }

    /**
     * @param <C>  cell type
     * @param <CT> cell value type
     */
    public abstract static class Builder<C extends Cell<CT>, CT> {
    	private final boolean hasTotals;

        protected Builder(CellGrid grid, String fieldName, boolean hasTotals) {
        	this.grid = grid;
            this.fieldName = fieldName;
            this.hasTotals = hasTotals;
        }

        public class DraggableHeader implements Draggable<Column<Record, ?>> {

            private final Column<Record, ?> column;
            private final GridHeaderCell cell;

            public DraggableHeader(Column<Record, ?> column, GridHeaderCell cell) {
                this.column = column;
                this.cell = cell;
            }

            @Override
            public Element getDraggableElement() {
                return cell.getCaptionElement();
            }

            @Override
            public Column<Record, ?> getValue() {
                return column;
            }
        }

        public static class ResizableHeader implements Resizable {
            private final CellGrid grid;
            private final Column<Record, ?> column;
            private final GridHeaderCell cell;

            public ResizableHeader(CellGrid grid, Column<Record, ?> column, GridHeaderCell cell) {
                this.grid = grid;
                this.column = column;
                this.cell = cell;
            }

            @Override
            public Element getResizeElement() {
                return cell.getRightResizeElement();
            }

            @Override
            public void setSize(int size) {
                grid.setColumnWidth(column, size, Style.Unit.PX);
            }

            @Override
            public int getSize(Element parent) {
                int startWidth = 100;
                while (parent != null) {
                    if (parent.getTagName().equalsIgnoreCase("th")) {
                        startWidth = parent.getAbsoluteRight() - parent.getAbsoluteLeft();
                        break;
                    }
                    parent = parent.getParentElement();
                }
                return startWidth;
            }
        }


        private static final String DEFAULT_WIDTH = "100px";

        protected final CellGrid grid;
        protected final String fieldName;
        private String caption;
        private String width = DEFAULT_WIDTH;
        private boolean useDefaultContextMenu = true;
        private ContextMenu<CT> contextMenu;
        private Comparator<Record> comparator;
        private boolean useDefaultColumnMenu = true;
        private ColumnMenu columnMenu;
        private boolean resizable = true;
        private boolean draggable = true;
        private boolean sortable = true;
        private boolean readonly = false;
        protected boolean nullable = false;
        private Cell<CT> cell;

        protected Builder<C, CT> self() {
        	return this;
        }

        public Builder<C, CT> setCaption(String caption) {
            this.caption = caption;
            return self();
        }

        public Builder<C, CT> setWidth(String width) {
        	if (width != null && !width.trim().isEmpty()) {
        		this.width = width;
        	}
            return self();
        }

        public Builder<C, CT> setComparator(Comparator<Record> comparator) {
            this.comparator = comparator;
            return self();
        }

        public Builder<C, CT> setColumnMenu(ColumnMenu columnMenu) {
            useDefaultColumnMenu = false;
            this.columnMenu = columnMenu;
            return self();
        }

        protected ColumnMenu getDefaultColumnMenu() {
            final ColumnMenu columnMenu = new ColumnMenuImpl(grid, null);
            setColumnMenu(columnMenu);
            return columnMenu;
        }

        public Builder<C, CT> setResizable(boolean resizable) {
            this.resizable = resizable;
            return self();
        }

        public Builder<C, CT> setDraggable(boolean draggable) {
            this.draggable = draggable;
            return self();
        }

        public Builder<C, CT> setContextMenu(ContextMenu<CT> contextMenu) {
            this.useDefaultColumnMenu = false;
            this.contextMenu = contextMenu;
            return self();
        }

        public Builder<C, CT> setNullable(boolean nullable) {
            this.nullable = nullable;
            return self();
        }

        public Builder<C, CT> setReadonly(boolean readonly) {
            this.readonly = readonly;
            return self();
        }

        public Builder<C, CT> setSortable(boolean sortable) {
            this.sortable = sortable;
            return self();
        }

        protected abstract C createCell();

        protected abstract GridColumn<CT> createColumn(Cell<CT> cell);

        public String getFieldName() {
            return fieldName;
        }

        public boolean isReadonly() {
            return readonly;
        }


        protected void initReadonlyContext(final C cell) {
            if (cell instanceof CanBeReadonly) {
                final CanBeReadonly hasParentReadonly = (CanBeReadonly) cell;
                hasParentReadonly.setReadonly(readonly);
                hasParentReadonly.setParentReadonly(grid.isReadonly());
                grid.addHandler(new ReadOnlyChangedEventHandler() {
                    @Override
                    public void onReadOnlyChanged(ReadOnlyChangedEvent event) {
                        hasParentReadonly.setParentReadonly(event.isReadonly());
                    }
                }, ReadOnlyChangedEvent.TYPE);
            }
        }

        private void initCellSupportedMultilineStyle(final C cell) {
            if (cell instanceof HasMultilineStyle) {
                final HasMultilineStyle hasMultilineStyle = (HasMultilineStyle) cell;
                hasMultilineStyle.setMultilineStyle(grid.isMultilineStyle());
                grid.addHandler(new MultilineStyleChangedEventHandler() {
                    @Override
                    public void onMultilineStyleChanged(MultilineStyleChangedEvent event) {
                        hasMultilineStyle.setMultilineStyle(event.isMultilineStyle());
                    }
                }, MultilineStyleChangedEvent.TYPE);
            }
        }

        private void initCellWithCache(final C cell) {
            if (cell instanceof HasViewDataCache) {
                final HasViewDataCache hasViewDataCache = (HasViewDataCache) cell;
                grid.addHandler(new CacheInvalidatedHandler() {
                    @Override
                    public void onCacheInvalidated(CacheInvalidatedEvent event) {
                        hasViewDataCache.clearViewDataCache();
                    }
                }, CacheInvalidatedEvent.TYPE
                );
            }
        }

        public GridColumn<CT> build() {
            C cell = createCell();

            initReadonlyContext(cell);
            initCellWithCache(cell);
            initCellSupportedMultilineStyle(cell);

            // Context menu
            if (useDefaultContextMenu) {
                contextMenu = getDefaultContextMenu();
            }
            GridColumn<CT> column;
            if (contextMenu != null) {
                CellExtendableWrapper<CT> wrapperCell = new CellExtendableWrapper<CT>(cell);
                wrapperCell.addBrowserEventListener(new ContextMenuHelper<CT>(contextMenu));
                this.cell = wrapperCell;
                column = createColumn(wrapperCell);
            } else {
                this.cell = cell;
                column = createColumn(cell);
            }

            // Sort
            if (sortable) {
                if (comparator == null) {
                    comparator = getDefaultComparator();
                }
                grid.setSortHandlerComparator(column, comparator);
            }

            // Column menu
            GridHeader header;
            if (useDefaultColumnMenu) {
                columnMenu = getDefaultColumnMenu();
            }
            GridHeaderCell headerCell = new GridHeaderCell(grid, column, columnMenu);
            if (draggable || resizable) {
                CellExtendableWrapper<String> wrappedCell = new CellExtendableWrapper<String>(headerCell);

                if (resizable) {
                    Resizable resizable = new ResizableHeader(grid, column, headerCell);
                    wrappedCell.addBrowserEventListener(new ResizeHelper<String>(resizable, new ResizeHelper.HorizontalPositionController()));
                }

                if (draggable) {
                    Draggable<?> draggable = new DraggableHeader(column, headerCell);
                    DropHelper dropHelper = new DropHelper(grid, column, headerCell);
                    grid.getDragAndDropController().addDropZone(dropHelper);
                    wrappedCell.addBrowserEventListener(new DragHelper<String>(draggable, grid.getDragAndDropController()));
                }

                header = new GridHeader(wrappedCell, caption);
            } else {
                header = new GridHeader(headerCell, caption);
            }

            Header<?> footer = null;
            if (hasTotals) {
            	footer = getFooter();
            }

            if (footer == null) {
            	grid.addColumn(column, header);
            } else {
            	grid.addColumn(column, header, footer);
            }

            if (this instanceof StatusColumn.Builder) {
            	// Status column should have additional style applied to header and footer
            	String style = header.getHeaderStyleNames();
            	if (style == null) {
            		style = StatusCell.RESOURCES.style().bStatusHeader();
            	} else {
            		style = style + " " + StatusCell.RESOURCES.style().bStatusHeader();
            	}
            	header.setHeaderStyleNames(style);
            	if (footer != null) {
            		String footerStyle = footer.getHeaderStyleNames();
            		if (footerStyle == null) {
                		footerStyle = StatusCell.RESOURCES.style().bStatusCell();
            		} else {
            			footerStyle = footerStyle + " " + StatusCell.RESOURCES.style().bStatusCell();
            		}
            		footer.setHeaderStyleNames(footerStyle);
            	}
            }

            grid.setColumnWidth(column, width);

            return column;
        }

        protected ContextMenu<CT> getDefaultContextMenu() {
            return null;
        }

        protected Comparator<Record> getDefaultComparator() {
            return new StringComparator(fieldName);
        }

        protected Header<?> getFooter() {
        	return new EmptyFooter();
        }
    }

}
